import * as THREE from "three"
import { SetMouseCursorCommand, DisableCursorMeshCommand } from "../command/cursor.command"
import CommandBinder from "../CommandBinder"
import configConst from "../constant/config.const"
import CanvasData from "../data/CanvasData"
import cursorEnum from "../enum/cursor.enum"
import pinColorEnum from "../enum/pinColor.enum"
import { ClickEvent, HoverEvent, UnhoverEvent } from "../event/GestureEvent"
import InstancedPinHeads from "../mesh/InstancedPinHeads"
import InstancedPinLines from "../mesh/InstancedPinLines"
import { PinClickedMessage, PinHoverChangeMessage } from "../message/pin.message"
import InputIniModule from "../module/InputIniModule"
import { pinItemData, matterItemData } from "../module/PinsModule"
import RaycasterModule from "../module/RaycasterModule"
import AnimatedNumber from "../object/AnimatedNumber"
import Vectors from "../three/Vectors"
import browserUtil from "../utils/browser.util"
import Comparator from "../utils/Comparator"
import funcUtil from "../utils/func.util"
import { RenderLayer } from "../utils/RenderLayers"
import PinRenderer from "./PinRenderer"
import de from "../math/49487"
import Logger from "../utils/Logger"

const ye = new Logger("InstancedPinRenderer")
function ce(
  e: {
    baseColor: THREE.Color
    hoverColor: THREE.Color
    dimmedColor: THREE.Color
  },
  t: string
) {
  switch (t) {
    case pinColorEnum.DEFAULT:
      return e.baseColor
    case pinColorEnum.DIMMED:
      return e.dimmedColor
    case pinColorEnum.HIGHLIGHTED:
      return e.hoverColor
  }
}
export default class InstancedPinRenderer extends PinRenderer {
  camera: THREE.PerspectiveCamera
  canvasData: CanvasData
  pins: Map<string, Map<string, pinItemData>>
  idToPin: Map<string, pinItemData>
  keyToRenderObjs: Map<
    string,
    {
      lines: InstancedPinLines
      pinHeads: InstancedPinHeads
      id: string
    }
  >
  updatePinHeadMatrix: (n: any) => void
  lastHoverId: string
  lastHoverType: string
  constructor(e: InputIniModule, t: THREE.PerspectiveCamera, i: CanvasData, s: RenderLayer, r: CommandBinder, o: RaycasterModule) {
    super(e, s, r, o)
    this.camera = t
    this.canvasData = i
    this.pins = new Map()
    this.idToPin = new Map()
    this.keyToRenderObjs = new Map()
    this.updatePinHeadMatrix = (() => {
      const e = new THREE.Vector3()
      const t = new THREE.Vector3()
      const i = new THREE.Vector3()
      const s = new THREE.Vector3()
      const r = new THREE.Vector3()
      const o = new THREE.Vector3()
      const a = new THREE.Vector3()
      return n => {
        const l = configConst.PlaneBufferGeometryConfg.pinHeadMesh.scale
        const c = this.getViewportScale(this.canvasData)
        const h = 1 + (l.responsiveness / 100) * (c - 1)
        this.camera.getWorldPosition(o)
        const d = this.camera.quaternion
        const u = this.canvasData
        for (const c of n) {
          a.copy(c.pinHeadObjPosition).add(c.anchorPosition)
          const n = o.distanceTo(a),
            p = l.maxSize - (l.maxSize - l.minSize) * de.C(n, l.nearBound, l.farBound)
          t.copy(a).project(this.camera),
            i.set(u.width / 2, u.height / 2, 1).multiply(t),
            s.set(p / 2, 0, 0).add(i),
            r.set(2 / u.width, 2 / u.height, 1).multiply(s)
          const m = r.unproject(this.camera).distanceTo(a) * h,
            g = funcUtil.getMax(m, 0.00001, m),
            f = c.geomScale || Vectors.UNIT
          e.set(g * f.x, g * f.y, g * f.z)
          c.pinHeadMatrix.compose(a, d, e)
        }
      }
    })()
  }
  dispose() {
    super.dispose()
  }
  activate(e) {
    if (!this.bindings.length) {
      super.activate(e)
      this.bindings.push(
        this.input.registerPriorityHandler(ClickEvent, InstancedPinHeads, (t, i, n) => {
          if (!n || void 0 === n.instanceId) return !0
          const s = i.renderedPins[n.instanceId]
          return e.broadcast(new PinClickedMessage(s.id, s.pinType)), !0
        })
      )
      if (!browserUtil.canTouch()) {
        this.bindings.push(
          this.input.registerMeshHandler(HoverEvent, Comparator.isType(InstancedPinHeads), (t, i, n) => {
            if (!n || !n.instanceId) return
            const s = i.renderedPins[n.instanceId]
            this.lastHoverId = s.id
            this.lastHoverType = s.pinType
            e.broadcast(new PinHoverChangeMessage(s.id, !0, s.pinType))
            e.commandBinder.issueCommand(new SetMouseCursorCommand(cursorEnum.FINGER))
            e.commandBinder.issueCommand(new DisableCursorMeshCommand(!0))
          }),
          this.input.registerMeshHandler(UnhoverEvent, Comparator.isType(InstancedPinHeads), () => {
            e.broadcast(new PinHoverChangeMessage(this.lastHoverId, !1, this.lastHoverType)),
              e.commandBinder.issueCommand(new SetMouseCursorCommand(null)),
              e.commandBinder.issueCommand(new DisableCursorMeshCommand(!1))
          })
        )
      }
    }
  }
  render(e: number) {
    for (const t of this.pins.values()) {
      const i: pinItemData[] = Array.from(t.values())
      if (!i.length) continue
      const n = this.keyFromPin(i[0])
      const s = this.keyToRenderObjs.get(n)
      if (!s) {
        ye.error("Expecting renderObjs for this key.", this.keyToRenderObjs, this.keyFromPin(i[0]))
        continue
      }
      let { lines, pinHeads } = s
      i.forEach(t => t.opacityAnimation.tick(e))
      this.updatePinHeadMatrix(i)
      if (lines.maxCount < i.length) {
        const e = lines.parent
        if (!e) {
          ye.error("Expecting a parent.")
          continue
        }
        e.remove(lines)
        lines.dispose()
        e.remove(pinHeads)
        this.input.unregisterMesh(pinHeads)
        const { backgroundTexture, maskTexture, overrideTexture } = i[0]
        pinHeads.dispose()
        Object.assign(s, this.createInstances(e, s.id, i.length + 16, backgroundTexture, maskTexture, overrideTexture))
        lines = s.lines
        pinHeads = s.pinHeads
      }
      lines.update(i, this.canvasData)
      pinHeads.update(i)
    }
    super.render(e)
  }
  updatePin(e: string, t: string, i: matterItemData, s?: THREE.Texture, r?, o?) {
    super.updatePin(e, t, i, s, r, o)
    let l = this.idToPin.get(e)
    const c = i.floorId
    const h = this.getColor(i.color).baseColor
    const d = `${c}_${t}_${l?.overrideTexture?.uuid || s.uuid}`
    if (!l) {
      l = Object.assign(Object.assign({ id: e }, i), {
        visible: !0,
        opacity: 1,
        pinType: t,
        opacityAnimation: new AnimatedNumber(1),
        backgroundTexture: s,
        maskTexture: r,
        pinHeadMatrix: new THREE.Matrix4(),
        pinHeadObjPosition: new THREE.Vector3(),
        pinColor: h,
        colorVariant: pinColorEnum.DEFAULT,
        overrideTexture: null,
        geomScale: null
      })
      this.idToPin.set(e, l)
      let o = this.pins.get(d) || new Map()
      this.pins.set(d, o)
      o.set(e, l)
      this.createRenderObjsForPin(l)
    }
    const u = this.keyFromPin(l)
    Object.assign(l, i, { pinType: t, textureId: s.uuid })
    this.changePinHeadGroupIfNeeded(u, d, l)
    l.pinHeadObjPosition.copy(l.stemNormal).setLength(l.stemLength)
    l.pinColor = ce(this.getColor(l.color), l.colorVariant)
    o && this.setPinVisible(e, o)
  }
  removePin(e) {
    super.removePin(e)
    const t = this.idToPin.get(e)
    if (!t) return
    this.idToPin.delete(e)
    const i = this.keyFromPin(t),
      n = this.pins.get(i)
    n && (n.delete(e), this.cleanupRenderObjIfNeeded(i))
  }
  removePinsByPredicate(e) {
    this.idToPin.forEach((t, i) => {
      e(t.pinType) && this.removePin(i)
    })
  }
  setPinVisible(e: string, t) {
    const i = this.idToPin.get(e)
    i ? (i.visible = t) : ye.error("setPinVisible on a pin that doesn't exist.", e)
  }
  setPinColorVariant(id: string, t: string) {
    const i = this.idToPin.get(id)
    i ? ((i.colorVariant = t), (i.pinColor = ce(this.getColor(i.color), t))) : ye.error("setPinColorVariant on a pin that doesn't exist.")
  }
  setPinColorVariants(e, t?) {
    this.idToPin.forEach((i, n) => {
      n !== t && this.setPinColorVariant(n, e)
    })
  }
  setPinColorVariantByType(e, t, i) {
    this.idToPin.forEach((n, s) => {
      n.pinType === e && s !== i && this.setPinColorVariant(s, t)
    })
  }
  setPinOpacity(e, t) {
    const i = this.idToPin.get(e)
    i ? (i.opacity = t) : ye.error("setPinOpacity on a pin that doesn't exist.")
  }
  fadePinOpacity(e, t) {
    const i = this.idToPin.get(e)
    i
      ? (t > 0 && this.setPinVisible(e, !0),
        i.opacityAnimation.modifyAnimation(i.opacityAnimation.value, t, 300).onComplete(() => {
          t <= 0 && this.setPinVisible(e, !1)
        }))
      : ye.error("setPinVisible on a pin that doesn't exist.", e)
  }
  setPinOpacityByType(e, t, i?) {
    this.idToPin.forEach((n, s) => {
      n.pinType === e && s !== i && this.setPinOpacity(s, t)
    })
  }
  fadePinOpacityByType(e, t, i = []) {
    this.idToPin.forEach((n, s) => {
      n.pinType !== e || i.includes(s) || this.fadePinOpacity(s, t)
    })
  }
  setPinRenderOverrides(e, t, i) {
    super.setPinRenderOverrides(e, t, i)
    const n = this.idToPin.get(e)
    if (!n) return void ye.error("setPinRenderOverrides on a pin that doesn't exist.")
    const s = this.keyFromPin(n)
    if (!this.keyToRenderObjs.get(s)) return void ye.error("Expecting renderObjs while setting override material")
    ;(n.overrideTexture = t), (n.geomScale = i)
    const r = this.keyFromPin(n)
    this.changePinHeadGroupIfNeeded(s, r, n)
  }
  pinHeadTransform(e) {
    const t = this.idToPin.get(e)
    return t ? t.pinHeadMatrix : (ye.error("pinHeadTransform on a pin that doesn't exist."), new THREE.Matrix4())
  }
  keyFromPin(e) {
    return `${e.floorId}_${e.pinType}_${e.overrideTexture ? e.overrideTexture.uuid : e.backgroundTexture.uuid}`
  }
  createRenderObjsForPin(e: pinItemData) {
    const { backgroundTexture, maskTexture, overrideTexture } = e
    const s = this.keyFromPin(e)
    if (this.keyToRenderObjs.get(s)) return
    const r: THREE.Object3D = this.getFloorContainer(e.floorId).userData.typeContainers[e.pinType]
    r.userData = r.userData || {}
    const o = e.overrideTexture ? e.overrideTexture.uuid : e.backgroundTexture.uuid
    if (!r.userData[o]) {
      const e = this.createInstances(r, o, 16, backgroundTexture, maskTexture, overrideTexture)
      r.userData[o] = e
      this.keyToRenderObjs.set(s, e)
    }
  }
  createInstances(e: THREE.Object3D, id: string, i: number, n: THREE.Texture, s: THREE.Texture, r: THREE.Texture) {
    const lines = new InstancedPinLines(i)
    lines.layers.mask = this.layer.mask

    e.add(lines)
    const pinHeads = new InstancedPinHeads(i, r || n, r ? null : s, !r)
    pinHeads.layers.mask = this.layer.mask
    e.add(pinHeads)
    //把标签mesh加入到事件监听mesh中
    this.input.registerMesh(pinHeads, !1)
    return { lines, pinHeads, id }
  }
  changePinHeadGroupIfNeeded(e, t, i) {
    if (e !== t) {
      let n = this.pins.get(t)
      n || ((n = new Map()), this.pins.set(t, n)), n.set(i.id, i), this.createRenderObjsForPin(i)
      const s = this.pins.get(e)
      s && (null == s || s.delete(i.id), this.cleanupRenderObjIfNeeded(e))
    }
  }
  cleanupRenderObjIfNeeded(e) {
    const t = this.pins.get(e),
      i = this.keyToRenderObjs.get(e)
    if (t && 0 === t.size && i) {
      const t = i.lines.parent
      if ((this.input.unregisterMesh(i.pinHeads), this.pins.delete(e), this.keyToRenderObjs.delete(e), !t)) return void ye.error("Expecting pinTypeObj!")
      ;(t.userData[i.id] = void 0), t.remove(i.lines), t.remove(i.pinHeads)
    }
  }
}
